/*
 * Copyright 2010 Kevin L'Huillier <klhuillier@gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.lhuillier.pwsafe.io.codec;


import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Date;
import org.lhuillier.pwsafe.model.PasswordHistory;
import org.lhuillier.pwsafe.model.PasswordHistoryEntry;
import org.lhuillier.pwsafe.model.RawField;
import org.lhuillier.pwsafe.model.Record;
import com.google.inject.Inject;


/**
 * Converts password history values. Password histories are stored as string
 * values with a special format.
 * <p>
 * <h4>Official documentation</h4>
 * Password History is an optional record. If it exists, it stores the
 * creation times and values of the last few passwords used in the current
 * entry, in the following format:
 *     "fmmnnTLPTLP...TLP"
 * where:
 * <ul>
 * <li>f  = {0,1} if password history is on/off</li>
 * <li>mm = 2 hexadecimal digits max size of history list (i.e. max = 255)</li>
 * <li>nn = 2 hexadecimal digits current size of history list</li>
 * <li>T  = Time password was set (time_t written out in %08x)</li>
 * <li>L  = 4 hexadecimal digit password length (in TCHAR)</li>
 * <li>P  = Password</li>
 * </ul>
 * No history being kept for a record can be represented either by the lack of
 * the PWH field (preferred), or by a header of _T("00000"):
 *     flag = 0, max = 00, num = 00
 */
class PwHistoryConverter implements FieldConverter {
    private static final byte TYPE_ID = 0x0f;
    private final BinConverter binConv;

    @Inject
    PwHistoryConverter(BinConverter binConv) {
        this.binConv = binConv;
    }

    @Override
    public byte handledTypeId() {
        return TYPE_ID;
    }

    @Override
    public void toRecord(RawField raw, Record record) {
        record.setPasswordHistory(decode(raw.getValue()));
    }
    
    @Override
    public void toRaw(Record record, RawRecord raw) {
        PasswordHistory value = record.getPasswordHistory();
        if (value != null) {
            raw.addField(new RawField(TYPE_ID, encode(value)));
        }
    }
    
    /**
     * Reads the complete password history from a field's data.
     * <p>
     * At a minimum, the values for enabled, max size, and current size must
     * exist if there is a PasswordHistory field. If this field is present,
     * but history is disabled, the field should contain: 00000
     */
    PasswordHistory decode(byte[] data) {
        ArrayList<PasswordHistoryEntry> entries =
                new ArrayList<PasswordHistoryEntry>();
        boolean enabled = data[0] == '1';
        // Current size is ignored
        int[] sizes = binConv.hexToIntArray(data, 1, 2);
        int maxSize = sizes[0];

        for (int off = 5; off < data.length;) {
            int[] timeBytes = binConv.hexToIntArray(data, off, 8);
            long millis = 1000L * (timeBytes[3] ^ (timeBytes[2] << 8)
                    ^ (timeBytes[1] << 16) ^ (timeBytes[0] << 24L));
            Date time = new Date(millis);
            int[] lens = binConv.hexToIntArray(data, off + 8, 4);
            int len = (lens[0] << 8) ^ lens[1];
            String password = binConv.readString(data, off + 12, len);
            entries.add(new PasswordHistoryEntry(time, password));
            off += 12 + len;
        }

        return new PasswordHistory(enabled, maxSize, entries);
    }

    byte[] encode(PasswordHistory history) {
        if (!history.isEnabled()) {
            return emptyHistory();
        }

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        result.write('1');
        addSizes(result, history);
        addEntries(result, history);
        return result.toByteArray();
    }

    private byte[] emptyHistory() {
        return new byte[] {'0', '0', '0', '0', '0'};
    }

    private void addSizes(ByteArrayOutputStream out, PasswordHistory history) {
        byte max = (byte) history.getMaxSize();
        byte size = (byte) history.getCurrentSize();
        byte[] maxBytes = binConv.writeByteToHex(max);
        byte[] sizeBytes = binConv.writeByteToHex(size);
        out.write(maxBytes, 0, 2);
        out.write(sizeBytes, 0, 2);
    }

    private void addEntries(ByteArrayOutputStream out,
            PasswordHistory history) {
        int offset = 5;

        for (PasswordHistoryEntry entry : history.getEntries()) {
            byte[] entryData = encodeEntry(entry);
            out.write(entryData, 0, entryData.length);
            offset += entryData.length;
        }
    }

    private byte[] encodeEntry(PasswordHistoryEntry entry) {
        byte[] password = entry.getPassword().getBytes();
        short pwLength = (short) password.length;
        byte[] pwLengthBytes = binConv.writeShortToHex(pwLength);
        int time = (int) (entry.getTimeSet().getTime() / 1000);
        byte[] timeBytes = binConv.writeIntToHex(time);

        byte[] result = new byte[pwLength + 12];
        System.arraycopy(timeBytes, 0, result, 0, 8);
        System.arraycopy(pwLengthBytes, 0, result, 8, 4);
        System.arraycopy(password, 0, result, 12, pwLength);
        return result;
    }
}
